#!/usr/bin/env python
# encoding: utf-8
#
# PyGadu - Pythonic Gadu-Gadu implementation
# Copyright (c) 2005,2006 Zygmunt Krynicki
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

import struct
import Util

class PacketError:
    pass

def bytesAsHex(bytes):
    return " ".join([ "%02X" % byte for byte in struct.unpack("<%dB" % len(bytes), bytes)])

def bytesAsText(bytes):
    return " ".join([(char.isalnum() and " %c" % char) or "--" for char in struct.unpack("<%dc" % len(bytes), bytes)])

def dumpBytes(bytes):
    print "Dump as hex (%3d bytes) :" % len(bytes), bytesAsHex(bytes)
    print "Dump as text            :", bytesAsText(bytes)
    
class UnknownPacketError(PacketError):
    def __init__(self, header, payload):
        self.header = header
        self.payload = payload
    def __str__(self):
        dump_h = bytesAsHex(self.header)
        dump_p = bytesAsText(self.payload)
        return "Unknown packet:\nHeader: %s\nPayload: %s\n" % (dump_h, dump_p)
     
class CorruptedPacketError(PacketError):
    def __init__(self, bytes):
        self.bytes = bytes
    def __str__(self):
        return "Corrupted packet (%d bytes)" % len(self.bytes)

# ---------------------------------------------------------- #

class IncomingPacket:
    def unpack(self, format_data, bytes, container = None):
        if container == None:
            container = self
        need_size = self.calcsize(format_data)
        if need_size > len(bytes):
            raise Exception("Bad packet upack data, expected %d got %d" % (need_size, len(bytes)))
        offset = 0
        for format_entry in format_data:
            if offset >= len(bytes):
                print "Exeeded format size!"
                break
            if len(format_entry) == 2:
                format, field_name = format_entry
                flags = []
            else:
                format, field_name, flags = format_entry
            if format == "s":
                try:
                    size = bytes[offset:].index('\0')
                    field_value = bytes[offset:offset+size]
                except ValueError:
                    size = 0
                    field_value = ""
            elif format == "S":
                try:
                    size = bytes[offset:].index('\0')
                    field_value = bytes[offset:offset+size]
                except ValueError:
                    size = len(bytes[offset:])
                    field_value = bytes[offset:]
            else:
                size = struct.calcsize(format)
                field_value = struct.unpack("<"+format, bytes[offset:offset+size])[0]
            offset += size
            for flag_mask, flag_name in flags:
                if field_value & flag_mask:
                    field_value ^= flag_mask
                    setattr(container, flag_name, True)
                    print "\tFlag %10s: set" % flag_name
                else:
                    setattr(container, flag_name, False)
                    print "\tFlag %10s: ---" % flag_name
            print "\t%s: %d [%s:%d]" % (field_name, field_value, format, size)
            setattr(container, field_name, field_value)
        if len(bytes) > offset:
            return bytes[offset:]
        else:
            return None
    def unpackString(self, bytes, length, name, container):
        if container == None:
            container = self
        if len(bytes) < length:
            raise Exception("Bad packet upack data, expected %d got %d" % (length, len(bytes)))
        value = struct.unpack("<%ds" % length, bytes)[0]
        #print "\tField %s: '%s' [s:%d]" % (name, value, format, length)
        setattr(container, name, value)
        if len(bytes) > length:
            return bytes[length:]
        else:
            return None
    def calcsize(self, format):
        return sum([struct.calcsize(format[0]) for format in format])

class Welcome(IncomingPacket):
    TYPE = 0x0001
    FORMAT = (("I", "seed"),)
    def __init__(self, bytes):
        result = self.unpack(self.FORMAT, bytes)
        assert result == None
    def __str__(self):
        return "Welcome, %d" % self.seed

class LoginOk:
    TYPE = 0x0003
    def __init__(self, bytes):
        pass
    def __str__(self):
        return "Login OK"

class LoginOkButEmailNeeded:
    TYPE = 0x0014
    def __init__(self, bytes):
        pass
    def __str__(self):
        return "Login OK but email needed"

class LoginFailed:
    TYPE = 0x0009
    def __init__(self, bytes):
        pass
    def __str__(self):
        return "Login Failed"

class Disconnected:
    TYPE = 0x000b
    def __init__(self, bytes):
        pass
    def __str__(self):
        return "Disconnecting"

class IncomingMessage:
    TYPE = 0x000a
    
    CLASS_QUEUED    = 0x0001
    CLASS_MESSAGE   = 0x0004
    CLASS_CHAT      = 0x0008
    CLASS_CTCP      = 0x0010
    CLASS_ACK       = 0x0020
    
    CLASS_MARTIANS  = ~ (CLASS_QUEUED | CLASS_MESSAGE | CLASS_CHAT | CLASS_CTCP | CLASS_ACK)
    
    def __init__(self, bytes):
        self.sender, self.seq, self.time, self.clazz, self.message = struct.unpack(
                "<iiii%ds" % (len(bytes) + 1 - (4*4+1)), bytes)
        self.unicode_message = Util.ggStringToUnicode(self.message)
    def isQueued(self):
        return self.clazz & self.CLASS_QUEUED
    def isNewMessage(self):
        return self.clazz & self.CLASS_MESSAGE
    def isChatMessage(self):
        return self.clazz & self.CLASS_CHAT
    def isCtcp(self):
        return self.clazz & self.CLASS_CTCP
    def isAck(self):
        return self.clazz & self.CLASS_ACK
    def getMartians(self):
        return self.clazz & self.CLASS_MARTIANS
    def __str__(self):
        return "\n".join([
                u"Incoming message: %s" % self.unicode_message,
                u"From:         %d" % self.sender,
                u"Seq number:   %d" % self.seq,
                u"Sent:         %d" % self.time,
                u"Class:        %d" % self.clazz,
                u"Queued:       %s" % (self.isQueued() and "Yes" or "No"),
                u"New message:  %s" % (self.isNewMessage() and "Yes" or "No"),
                u"Chat message: %s" % (self.isChatMessage() and "Yes" or "No"),
                u"CTCP:         %s" % (self.isCtcp() and "Yes" or "No"),
                u"Ack:          %s" % (self.isAck() and "Yes" or "No"),
                u"Martians:     %x" % self.getMartians()])

class OutgoingMessage:
    TYPE = 0x000b
    CLASS_QUEUED    = 0x0001
    CLASS_MESSAGE   = 0x0004
    CLASS_CHAT      = 0x0008
    CLASS_CTCP      = 0x0010
    CLASS_ACK       = 0x0020

    CLASS_MARTIANS  = 0 ^ (CLASS_QUEUED | CLASS_MESSAGE | CLASS_CHAT | CLASS_CTCP | CLASS_ACK)
    
    def __init__(self, recipient, seq, clazz, unicode_message):
        self.recipient = recipient
        self.seq = seq
        self.clazz = clazz
        self.unicode_message = unicode(unicode_message)
        self.message = Util.unicodeToGgString(self.unicode_message[:1980])
        self.bytes = struct.pack(
                "<iii%ds" % (len(self.message) + 1), recipient, seq, clazz, self.message)
    def __str__(self):
        return u"Outgoing message %d, %d, %d, '%s'" % (
                self.recipient, self.seq, self.clazz, self.unicode_message)

class OutgoingMessageAck:
    TYPE = 0x0005

    ACK_BLOCKED       = 0x0001
    ACK_DELIVERED     = 0x0002
    ACK_QUEUED        = 0x0003
    ACK_MBOXFULL      = 0x0004
    ACK_NOT_DELIVERED = 0x0006

    ACK_NOT_MARTIANS      = (ACK_BLOCKED, ACK_DELIVERED, ACK_QUEUED, ACK_MBOXFULL, ACK_NOT_DELIVERED)

    STATUS_TEXT = {
        ACK_BLOCKED:       "blocked",
        ACK_DELIVERED:     "delivered",
        ACK_QUEUED:        "queued",
        ACK_MBOXFULL:      "mailbox full",
        ACK_NOT_DELIVERED: "not delivered"
    }

    def __init__(self, bytes):
        self.status, self.recipient, self.seq = struct.unpack("<iii", bytes)
    def __str__(self):
        if self.STATUS_TEXT.has_key(self.status):
            return u"Outgoing message ack: %s, recipient: %d, seq: %d" % (
                    self.STATUS_TEXT[self.status], self.recipient, self.seq)
        else:
            return u"Outgoing message ack: MARTIANS<%d>, recipient: %d, seq: %d" % (
                    self.status, self.recipient, self.seq)
            

class Login60:
    TYPE = 0x0015

    VERSION = {
        "7.0 (build 22)": 0x27,
        "6.0" : 0x20
        }
    
    def __init__(self, version, uin, hash, status, unicode_desc="", local_ip=0, local_port=0, ext_ip=0, ext_port=0, image_size=0, time=0):
        self.version = version
        self.uin = uin
        self.hash = hash
        self.status = status
        self.unicode_desc = unicode_desc
        self.desc = Util.unicodeToGgString(unicode_desc)
        self.bytes = struct.pack(
                "<IIIIBIhIhBB%dsI" % (len(self.desc) + 1), 
                uin, hash, status, version, 0x00, 
                local_ip, local_port, ext_ip, ext_port,
                image_size, 0xBE, self.desc, time)
    def __str__(self):
        return "Login60: %d, %d, %d, %d, '%s'" % (self.uin, self.version, self.hash, self.status, self.unicode_desc)


class NotifyFirst:
    TYPE = 0x000F
    def __init__(self, buddies):
        self.bytes = "".join([struct.pack("<IB", buddy.uin, buddy.type) for buddy in buddies])
    def __str__(self):
        return "NotifyFirst"

class NotifyLast:
    TYPE = 0x0010
    def __init__(self, buddies):
        self.bytes = "".join([struct.pack("<IB", buddy.uin, buddy.type) for buddy in buddies])
    def __str__(self):
        return "NotifyLast"

class NotifyInfo:
    def __str__(self):
        return "NotifyInfo: %d is %#x" % (self.uin, self.status)

class NotifyReply:
    TYPE = 0x000c
    def __init__(self, bytes):
        pass
    def __str__(self):
        return "NotifyReply"

class Status:
    NOT_AVAIL   = 0x0001
    AVAIL       = 0x0002
    BUSY        = 0x0003
    INVISIBLE   = 0x0014
    NOT_AVAIL_D = 0x0015
    AVAIL_D     = 0x0004
    BUSY_D      = 0x0005
    INVISIBLE_D = 0x0016
    BLOCKED     = 0x0006
    FRIENDS_MASK= 0x8000
        
class NotifyReply60(IncomingPacket):
    TYPE = 0x0011
    BASE_FORMAT = (
            #("I", "uin", ((0x08000000, "f_era_omnix"), (0x10000000, "f_unknown1"), (0x20000000, "f_unknown2"), (0x40000000, "f_voice"))),
            #("I", "uin", [ (1L << 24+i, "x_wojtek_%d__%08X" % (i, 1L << 24+i)) for i in range(8)]),
            ("I", "uin", (
                (0x01000000, "f_wojtek_01"),
                (0x02000000, "f_wojtek_02"),
                (0x04000000, "f_wojtek_04"),
                (0x08000000, "f_era_omnix"),
                (0x10000000, "f_unknown1"),
                (0x20000000, "f_unknown2"),
                (0x40000000, "f_voice"),
                (0x80000000, "f_wojtek_80")
                )),
            ("B", "status"),
            ("I", "remote_ip"),
            ("h", "remote_port"),
            ("B", "version"),
            ("B", "image_size"),
            ("B", "unknown1"))
    EX_FORMAT = (
            ("B", "extra_size"),)
    def __init__(self, bytes):
        self.notify_list= []
        rest = bytes
        while rest:
            info = NotifyInfo()
            rest = self.unpack(self.BASE_FORMAT, rest, info)
            if info.status in (Status.NOT_AVAIL_D, Status.INVISIBLE_D, Status.BUSY_D, Status.AVAIL_D):
                rest = self.unpack(self.EX_FORMAT, rest, info)
                rest = rest[info.extra_size:]
                if len(rest) == 0:
                    if rest[-4] == '\0':
                        info.desc = rest[:-4]
                        info.time = struct.unpack("<i", rest[-4:])[0]
                    else:
                        info.desc = rest
                        info.time = None
                else:
                    info.time = None
                    info.desc = None
            else:
                info.desc = ""
                info.unicode_desc = u""
            self.notify_list.append(info)
    def __str__(self):
        return "NotifyReply60: %s" % ", ".join([str(info) for info in self.notify_list])

class EmptyBuddyList:
    TYPE = 0x0012
    def __init__(self):
        self.bytes = ""
    def __str__(self):
        return "EmptyBuddyList"

class Ping:
    TYPE = 0x0008
    def __init__(self):
        self.bytes = ""
    def __str__(self):
        return "Ping"

class Pong:
    TYPE = 0x0007
    def __init__(self, bytes):
        pass
    def __str__(self):
        return "Pong"
    
class NewStatus:
    TYPE = 0x0002
    
    NOT_AVAIL   = 0x0001
    AVAIL       = 0x0002
    BUSY        = 0x0003
    INVISIBLE   = 0x0014
    NOT_AVAIL_D = 0x0015
    AVAIL_D     = 0x0004
    BUSY_D      = 0x0005
    INVISIBLE_D = 0x0016
    BLOCKED     = 0x0006
    FRIENDS_MASK= 0x8000
    def __init__(self, status, unicode_desc="", time=0):
        self.status = status
        self.unicode_desc = unicode_desc
        self.desc = Util.unicodeToGgString(unicode_desc)[:70] # At most 70 chars
        self.time = time
        if status in (Status.NOT_AVAIL_D, Status.AVAIL_D, Status.BUSY_D, Status.INVISIBLE_D):
            self.bytes = struct.pack("<i%dsi" % (len(self.desc) + 1), status, self.desc, time)
        else:
            self.bytes = struct.pack("<ii", status, time)
    def __str__(self):
        return u"New status: %d '%s' %d" % (self.status, self.unicode_desc, self.time)

class Status50(IncomingPacket):
    TYPE = 0x0002
    BASE_FORMAT = (
            ("I", "uin", (
                (0x01000000, "f_wojtek_01"),
                (0x02000000, "f_wojtek_02"),
                (0x04000000, "f_wojtek_04"),
                (0x08000000, "f_era_omnix"),
                (0x10000000, "f_unknown1"),
                (0x20000000, "f_unknown2"),
                (0x40000000, "f_voice"),
                (0x80000000, "f_wojtek_80")
                )),
            ("I", "status")
            )
    def __init__(self, bytes):
        rest = self.unpack(self.BASE_FORMAT, bytes)
        if rest:
            print "Status50, extra payload: %s" % bytesAsHex(rest)
            print ".......................: %s" % bytesAsText(rest)
        self.unicode_desc = ""
    def __str__(self):
        return u"Status50: %d %d '%s'" % (self.uin, self.status, self.unicode_desc)

class Status60(IncomingPacket):
    TYPE = 0x000f
    BASE_FORMAT = (
            ("I", "uin", (
                (0x01000000, "f_wojtek_01"),
                (0x02000000, "f_wojtek_02"),
                (0x04000000, "f_wojtek_04"),
                (0x08000000, "f_era_omnix"),
                (0x10000000, "f_unknown1"),
                (0x20000000, "f_unknown2"),
                (0x40000000, "f_voice"),
                (0x80000000, "f_wojtek_80")
                )),
            ("B", "status"),
            ("I", "remote_ip"),
            ("h", "remote_port"),
            ("B", "version"),
            ("B", "image_size"),
            ("B", "unknown1")
            )
    def __init__(self, bytes):
        rest = self.unpack(self.BASE_FORMAT, bytes)
        if rest:
            print "Status60, extra payload: %s" % bytesAsHex(rest)
            print ".......................: %s" % bytesAsText(rest)
        self.unicode_desc = ""
    def __str__(self):
        return u"Status60: %d %d '%s'" % (self.uin, self.status, self.unicode_desc)
   
incoming_packets = dict()
for p in IncomingMessage, OutgoingMessageAck, Welcome, LoginOk, LoginFailed, LoginOkButEmailNeeded, Pong, Disconnected, Status50, Status60, NotifyReply, NotifyReply60:
    incoming_packets[p.TYPE] = p
